Completed
Push — master ( 532380...d24750 )
by thomas
01:06
created

WalletSweeper.createTransaction   A

Complexity

Conditions 1
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
c 2
b 1
f 0
nc 2
dl 0
loc 5
rs 9.4285
nop 1
1
var UnspentOutputFinder = require('./unspent_output_finder');
2
var bitcoin = require('bitcoinjs-lib');
3
var bip39 = require("bip39");
4
var CryptoJS = require('crypto-js');
5
var blocktrail = require('./blocktrail');
6
var EncryptionMnemonic = require('./encryption_mnemonic');
7
var Encryption = require('./encryption');
8
var walletSDK = require('./wallet');
9
var _ = require('lodash');
10
var q = require('q');
11
var async = require('async');
12
13
/**
14
 *
15
 * @param backupData
16
 * @param bitcoinDataClient
17
 * @param options
18
 * @constructor
19
 */
20
var WalletSweeper = function(backupData, bitcoinDataClient, options) {
21
    /* jshint -W071, -W074 */
22
    var self = this;
23
    this.defaultSettings = {
24
        network: 'btc',
25
        testnet: false,
26
        regtest: false,
27
        logging: false,
28
        bitcoinCash: false,
29
        sweepBatchSize: 200
30
    };
31
    this.settings = _.merge({}, this.defaultSettings, options);
32
    this.bitcoinDataClient = bitcoinDataClient;
33
    this.utxoFinder = new UnspentOutputFinder(bitcoinDataClient, this.settings);
34
    this.sweepData = null;
35
36
    // set the bitcoinlib network
37
    if (typeof options.network === "object") {
38
        this.network = options.network;
39
    } else {
40
        this.network = this.getBitcoinNetwork(this.settings.network, this.settings.testnet, this.settings.regtest);
41
    }
42
43
    backupData.walletVersion = backupData.walletVersion || 2;   //default to version 2 wallets
44
45
    var usePassword = false;
46
47
    // validate backup data, cleanup input, and prepare seeds
48
    if (!Array.isArray(backupData.blocktrailKeys)) {
49
        throw new Error('blocktrail pub keys are required (must be type Array)');
50
    }
51
52
    switch (backupData.walletVersion) {
53
        case 1:
54
            if (typeof backupData.primaryMnemonic === "undefined" || !backupData.primaryMnemonic) {
55
                throw new Error('missing primary mnemonic for version 1 wallet');
56
            }
57
            if (typeof backupData.backupMnemonic === "undefined" || !backupData.backupMnemonic) {
58
                throw new Error('missing backup mnemonic for version 1 wallet');
59
            }
60
            if (typeof backupData.primaryPassphrase === "undefined") {
61
                throw new Error('missing primary passphrase for version 1 wallet');
62
            }
63
64
            // cleanup copy paste errors from mnemonics
65
            backupData.primaryMnemonic = backupData.primaryMnemonic.trim()
66
                .replace(new RegExp("\r\n", 'g'), " ")
67
                .replace(new RegExp("\n", 'g'), " ")
68
                .replace(/\s+/g, " ");
69
            backupData.backupMnemonic = backupData.backupMnemonic.trim()
70
                .replace(new RegExp("\r\n", 'g'), " ")
71
                .replace(new RegExp("\n", 'g'), " ")
72
                .replace(/\s+/g, " ");
73
        break;
74
75
        case 2:
76
        case 3:
77
            if (typeof backupData.encryptedPrimaryMnemonic === "undefined" || !backupData.encryptedPrimaryMnemonic) {
78
                throw new Error('missing encrypted primary seed for version 2 wallet');
79
            }
80
            if (typeof backupData.backupMnemonic === "undefined" || (!backupData.backupMnemonic && backupData.backupMnemonic !== false)) {
81
                throw new Error('missing backup seed for version 2 wallet');
82
            }
83
            //can either recover with password and password encrypted secret, or with encrypted recovery secret and a decryption key
84
            usePassword = typeof backupData.password !== "undefined" && backupData.password !== null;
85
            if (usePassword) {
86
                if (typeof backupData.passwordEncryptedSecretMnemonic === "undefined" || !backupData.passwordEncryptedSecretMnemonic) {
87
                    throw new Error('missing password encrypted secret for version 2 wallet');
88
                }
89
                if (typeof backupData.password === "undefined") {
90
                    throw new Error('missing primary passphrase for version 2 wallet');
91
                }
92
            } else {
93
                if (typeof backupData.encryptedRecoverySecretMnemonic === "undefined" || !backupData.encryptedRecoverySecretMnemonic) {
94
                    throw new Error('missing encrypted recovery secret for version 2 wallet (recovery without password)');
95
                }
96
                if (!backupData.recoverySecretDecryptionKey) {
97
                    throw new Error('missing recovery secret decryption key for version 2 wallet (recovery without password)');
98
                }
99
            }
100
101
            // cleanup copy paste errors from mnemonics
102
            backupData.encryptedPrimaryMnemonic = backupData.encryptedPrimaryMnemonic.trim()
103
                .replace(new RegExp("\r\n", 'g'), " ")
104
                .replace(new RegExp("\n", 'g'), " ")
105
                .replace(/\s+/g, " ");
106
            backupData.backupMnemonic = (backupData.backupMnemonic || "").trim()
107
                .replace(new RegExp("\r\n", 'g'), " ")
108
                .replace(new RegExp("\n", 'g'), " ")
109
                .replace(/\s+/g, " ");
110
            if (backupData.recoverySecretDecryptionKey) {
111
                backupData.recoverySecretDecryptionKey = backupData.recoverySecretDecryptionKey.trim()
112
                    .replace(new RegExp("\r\n", 'g'), " ")
113
                    .replace(new RegExp("\n", 'g'), " ")
114
                    .replace(/\s+/g, " ");
115
            }
116
            if (usePassword) {
117
                backupData.passwordEncryptedSecretMnemonic = backupData.passwordEncryptedSecretMnemonic.trim()
118
                    .replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
119
            } else {
120
                backupData.encryptedRecoverySecretMnemonic = backupData.encryptedRecoverySecretMnemonic.trim()
121
                    .replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
122
            }
123
124
        break;
125
126
        default:
127
            throw new Error('Wrong version [' + backupData.walletVersion + ']');
128
    }
129
130
131
    // create BIP32 HDNodes for the Blocktrail public keys
132
    this.blocktrailPublicKeys = {};
133
    _.each(backupData.blocktrailKeys, function(blocktrailKey) {
134
        self.blocktrailPublicKeys[blocktrailKey['keyIndex']] = bitcoin.HDNode.fromBase58(blocktrailKey['pubkey'], self.network);
135
    });
136
137
    // convert the primary and backup mnemonics to seeds (using BIP39)
138
    var primarySeed, backupSeed, secret;
139
    switch (backupData.walletVersion) {
140
        case 1:
141
            primarySeed = bip39.mnemonicToSeed(backupData.primaryMnemonic, backupData.primaryPassphrase);
142
            backupSeed = bip39.mnemonicToSeed(backupData.backupMnemonic, "");
143
        break;
144
145
        case 2:
146
            // convert mnemonics to hex (bip39) and then base64 for decryption
147
            backupData.encryptedPrimaryMnemonic = blocktrail.convert(bip39.mnemonicToEntropy(backupData.encryptedPrimaryMnemonic), 'hex', 'base64');
148
            if (usePassword) {
149
                backupData.passwordEncryptedSecretMnemonic = blocktrail.convert(
150
                    bip39.mnemonicToEntropy(backupData.passwordEncryptedSecretMnemonic), 'hex', 'base64');
151
            } else {
152
                backupData.encryptedRecoverySecretMnemonic = blocktrail.convert(
153
                    bip39.mnemonicToEntropy(backupData.encryptedRecoverySecretMnemonic), 'hex', 'base64');
154
            }
155
156
            // decrypt encryption secret
157
            if (usePassword) {
158
                secret = CryptoJS.AES.decrypt(backupData.passwordEncryptedSecretMnemonic, backupData.password).toString(CryptoJS.enc.Utf8);
159
            } else {
160
                secret = CryptoJS.AES.decrypt(backupData.encryptedRecoverySecretMnemonic, backupData.recoverySecretDecryptionKey).toString(CryptoJS.enc.Utf8);
161
            }
162
163
            if (!secret) {
164
                throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
165
            }
166
167
            // now finally decrypt the primary seed and convert to buffer (along with backup seed)
168
            primarySeed = new Buffer(CryptoJS.AES.decrypt(backupData.encryptedPrimaryMnemonic, secret).toString(CryptoJS.enc.Utf8), 'base64');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
169
170
            if (backupData.backupMnemonic) {
171
                backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
172
            }
173
174
        break;
175
176
        case 3:
177
            // convert mnemonics to hex (bip39) and then base64 for decryption
178
            backupData.encryptedPrimaryMnemonic = EncryptionMnemonic.decode(backupData.encryptedPrimaryMnemonic);
179
            if (usePassword) {
180
                backupData.passwordEncryptedSecretMnemonic = EncryptionMnemonic.decode(backupData.passwordEncryptedSecretMnemonic);
181
            } else {
182
                backupData.encryptedRecoverySecretMnemonic = EncryptionMnemonic.decode(backupData.encryptedRecoverySecretMnemonic);
183
            }
184
185
            // decrypt encryption secret
186
            if (usePassword) {
187
                secret = Encryption.decrypt(backupData.passwordEncryptedSecretMnemonic, new Buffer(backupData.password));
188
            } else {
189
                secret = Encryption.decrypt(backupData.encryptedRecoverySecretMnemonic, new Buffer(backupData.recoverySecretDecryptionKey, 'hex'));
190
            }
191
192
            if (!secret) {
193
                throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
194
            }
195
196
            // now finally decrypt the primary seed and convert to buffer (along with backup seed)
197
            primarySeed = Encryption.decrypt(backupData.encryptedPrimaryMnemonic, secret);
198
            if (backupData.backupMnemonic) {
199
                backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
200
            }
201
202
        break;
203
204
        default:
205
            throw new Error('Wrong version [' + backupData.walletVersion + ']');
206
    }
207
208
    // convert the primary and backup seeds to private keys (using BIP32)
209
    this.primaryPrivateKey = bitcoin.HDNode.fromSeedBuffer(primarySeed, this.network);
210
211
    if (backupSeed) {
212
        this.backupPrivateKey = bitcoin.HDNode.fromSeedBuffer(backupSeed, this.network);
213
        this.backupPublicKey = this.backupPrivateKey.neutered();
214
    } else {
215
        this.backupPrivateKey = false;
216
        this.backupPublicKey = bitcoin.HDNode.fromBase58(backupData.backupPublicKey, this.network);
217
    }
218
219
    if (this.settings.logging) {
220
        console.log('using password method: ' + usePassword);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
221
        console.log("Primary Prv Key: " + this.primaryPrivateKey.toBase58());
222
        console.log("Primary Pub Key: " + this.primaryPrivateKey.neutered().toBase58());
223
        console.log("Backup Prv Key: " + (this.backupPrivateKey ? this.backupPrivateKey.toBase58() : null));
224
        console.log("Backup Pub Key: " + this.backupPublicKey.toBase58());
225
    }
226
};
227
228
229
/**
230
 * returns an appropriate bitcoin-js lib network
231
 *
232
 * @param network
233
 * @param testnet
234
 * @param regtest
235
 * @returns {*[]}
236
 */
237
WalletSweeper.prototype.getBitcoinNetwork =  function(network, testnet, regtest) {
238
    switch (network.toLowerCase()) {
239
        case 'btc':
240
        case 'bitcoin':
241
            if (regtest) {
242
                return bitcoin.networks.regtest;
243
            } else if (testnet) {
244
                return bitcoin.networks.testnet;
245
            } else {
246
                return bitcoin.networks.bitcoin;
247
            }
248
        break;
0 ignored issues
show
Unused Code introduced by
This break statement is unnecessary and may be removed.
Loading history...
249
        case 'tbtc':
250
        case 'bitcoin-testnet':
251
            return bitcoin.networks.testnet;
252
        default:
253
            throw new Error("Unknown network " + network);
254
    }
255
};
256
257
/**
258
 * gets the blocktrail pub key for the given path from the stored array of pub keys
259
 *
260
 * @param path
261
 * @returns {boolean}
262
 */
263
WalletSweeper.prototype.getBlocktrailPublicKey = function(path) {
264
    path = path.replace("m", "M");
265
    var keyIndex = path.split("/")[1].replace("'", "");
266
267
    if (!this.blocktrailPublicKeys[keyIndex]) {
268
        throw new Error("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
269
    }
270
271
    return this.blocktrailPublicKeys[keyIndex];
272
};
273
274
/**
275
 * generate multisig address and redeem script for given path
276
 *
277
 * @param path
278
 * @returns {{address, redeem: *, witness: *}}
279
 */
280
WalletSweeper.prototype.createAddress = function(path) {
281
    //ensure a public path is used
282
    path = path.replace("m", "M");
283
    var keyIndex = path.split("/")[1].replace("'", "");
284
    var scriptType = parseInt(path.split("/")[2]);
285
286
    //derive the primary pub key directly from the primary priv key
287
    var primaryPubKey = walletSDK.deriveByPath(this.primaryPrivateKey, path, "m");
288
    //derive the backup pub key directly from the backup priv key (unharden path)
289
    var backupPubKey = walletSDK.deriveByPath(this.backupPublicKey, path.replace("'", ""), "M");
290
    //derive a pub key for this path from the blocktrail pub key
291
    var blocktrailPubKey = walletSDK.deriveByPath(this.getBlocktrailPublicKey(path), path, "M/" + keyIndex + "'");
292
293
    //sort the keys and generate a multisig redeem script and address
294
    var multisigKeys = walletSDK.sortMultiSigKeys([
295
        primaryPubKey.keyPair.getPublicKeyBuffer(),
296
        backupPubKey.keyPair.getPublicKeyBuffer(),
297
        blocktrailPubKey.keyPair.getPublicKeyBuffer()
298
    ]);
299
300
    var multisig = bitcoin.script.multisig.output.encode(2, multisigKeys);
301
    var redeemScript, witnessScript;
302
    if (this.network !== "bitcoincash" && scriptType === walletSDK.CHAIN_BTC_SEGWIT) {
303
        witnessScript = multisig;
304
        redeemScript = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(witnessScript));
305
    } else {
306
        witnessScript = null;
307
        redeemScript = multisig;
308
    }
309
    var scriptHash = bitcoin.crypto.hash160(redeemScript);
310
    var scriptPubKey = bitcoin.script.scriptHash.output.encode(scriptHash);
311
312
    var network = this.network;
313
    if (typeof this.network !== "undefined") {
314
        network = this.network;
315
    }
316
    var address = bitcoin.address.fromOutputScript(scriptPubKey, network, !!this.settings.bitcoinCash);
317
318
    // Insight nodes want nothing to do with 'bitcoin:' or 'bitcoincash:' prefixes
319
    address = address.replace('bitcoin:', '').replace('bitcoincash:', '');
320
321
    //@todo return as buffers
322
    return {address: address.toString(), redeem: redeemScript, witness: witnessScript};
323
};
324
325
/**
326
 * create a batch of multisig addresses
327
 *
328
 * @param start
329
 * @param count
330
 * @param keyIndex
331
 * @param chain
332
 * @returns {{}}
333
 */
334
WalletSweeper.prototype.createBatchAddresses = function(start, count, keyIndex, chain) {
335
    var self = this;
336
    var addresses = {};
337
338
    return q.all(_.range(0, count).map(function(i) {
339
        //create a path subsequent address
340
        var path =  "M/" + keyIndex + "'/" + chain + "/" + (start + i);
341
        var multisig = self.createAddress(path);
342
        addresses[multisig['address']] = {
343
            redeem: multisig['redeem'],
344
            witness: multisig['witness'],
345
            path: path
346
        };
347
    })).then(function() {
348
        return addresses;
349
    });
350
};
351
352
WalletSweeper.prototype.discoverWalletFunds = function(increment, cb) {
353
    var self = this;
354
    var totalBalance = 0;
355
    var totalUTXOs = 0;
356
    var totalAddressesGenerated = 0;
357
    var addressUTXOs = {};    //addresses and their utxos, paths and redeem scripts
358
    if (typeof increment === "undefined") {
359
        increment = this.settings.sweepBatchSize;
360
    }
361
362
    var deferred = q.defer();
363
    deferred.promise.nodeify(cb);
364
365
    var checkChain;
366
    if (this.network === "bitcoincash") {
367
        checkChain = [0, 1];
368
    } else {
369
        checkChain = [0, 1, 2];
370
    }
371
372
    async.nextTick(function() {
373
        //for each blocktrail pub key, do fund discovery on batches of addresses
374
        async.eachSeries(Object.keys(self.blocktrailPublicKeys), function(keyIndex, done) {
375
            async.eachSeries(checkChain, function(chain, done) {
376
                var i = 0;
377
                var hasTransactions = false;
378
379
                async.doWhilst(function(done) {
380
                    //do
381
                    if (self.settings.logging) {
382
                        console.log("generating addresses " + i + " -> " + (i + increment) + " using blocktrail key index: " + keyIndex + ", chain: " + chain);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
383
                    }
384
                    deferred.notify({
385
                        message: "generating addresses " + i + " -> " + (i + increment) + "",
386
                        increment: increment,
387
                        btPubKeyIndex: keyIndex,
388
                        chain: chain,
389
                        //addresses: [],
390
                        totalAddresses: totalAddressesGenerated,
391
                        addressUTXOs: addressUTXOs,
392
                        totalUTXOs: totalUTXOs,
393
                        totalBalance: totalBalance
394
                    });
395
396
                    async.nextTick(function() {
397
                        self.createBatchAddresses(i, increment, keyIndex, chain)
398
                            .then(function(batch) {
399
                                totalAddressesGenerated += Object.keys(batch).length;
400
401
                                if (self.settings.logging) {
402
                                    console.log("starting fund discovery for " + increment + " addresses...");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
403
                                }
404
405
                                deferred.notify({
406
                                    message: "starting fund discovery for " + increment + " addresses",
407
                                    increment: increment,
408
                                    btPubKeyIndex: keyIndex,
409
                                    //addresses: addresses,
410
                                    totalAddresses: totalAddressesGenerated,
411
                                    addressUTXOs: addressUTXOs,
412
                                    totalUTXOs: totalUTXOs,
413
                                    totalBalance: totalBalance
414
                                });
415
416
                                //get the unspent outputs for this batch of addresses
417
                                return self.bitcoinDataClient.batchAddressHasTransactions(_.keys(batch)).then(function(_hasTransactions) {
418
                                    hasTransactions = _hasTransactions;
419
                                    if (self.settings.logging) {
420
                                        console.log("batch " + (hasTransactions ? "has" : "does not have") + " transactions...");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
421
                                    }
422
423
                                    return q.when(hasTransactions)
424
                                        .then(function(hasTransactions) {
425
                                            if (!hasTransactions) {
426
                                                return;
427
                                            }
428
429
                                            //get the unspent outputs for this batch of addresses
430
                                            return self.utxoFinder.getUTXOs(_.keys(batch)).then(function(utxos) {
431
                                                // save the address utxos, along with relevant path and redeem script
432
                                                _.each(utxos, function(outputs, address) {
433
                                                    var witnessScript = null;
434
                                                    if (typeof batch[address]['witness'] !== 'undefined') {
435
                                                        witnessScript = batch[address]['witness'];
436
437
                                                    }
438
                                                    addressUTXOs[address] = {
439
                                                        path: batch[address]['path'],
440
                                                        redeem: batch[address]['redeem'],
441
                                                        witness: witnessScript,
442
                                                        utxos: outputs
443
                                                    };
444
445
                                                    totalUTXOs += outputs.length;
446
447
                                                    //add up the total utxo value for all addresses
448
                                                    totalBalance = _.reduce(outputs, function(carry, output) {
449
                                                        return carry + output['value'];
450
                                                    }, totalBalance);
451
452
                                                    if (self.settings.logging) {
453
                                                        console.log("found " + outputs.length + " unspent outputs for address: " + address);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
454
                                                    }
455
                                                });
456
457
                                                deferred.notify({
458
                                                    message: "discovering funds",
459
                                                    increment: increment,
460
                                                    btPubKeyIndex: keyIndex,
461
                                                    totalAddresses: totalAddressesGenerated,
462
                                                    addressUTXOs: addressUTXOs,
463
                                                    totalUTXOs: totalUTXOs,
464
                                                    totalBalance: totalBalance
465
                                                });
466
                                            });
467
                                        })
468
                                        ;
469
                                });
470
                            })
471
                            .then(
472
                                function() {
473
                                    //ready for the next batch
474
                                    i += increment;
475
                                    async.nextTick(done);
476
                                },
477
                                function(err) {
478
                                    done(err);
479
                                }
480
                            )
481
                        ;
482
                    });
483
                }, function() {
484
                    //while
485
                    return hasTransactions;
486
                }, function(err) {
487
                    //all done
488
                    if (err) {
489
                        console.log("batch complete, but with errors", err.message);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
490
491
                        deferred.notify({
492
                            message: "batch complete, but with errors: " + err.message,
493
                            error: err,
494
                            increment: increment,
495
                            btPubKeyIndex: keyIndex,
496
                            totalAddresses: totalAddressesGenerated,
497
                            addressUTXOs: addressUTXOs,
498
                            totalUTXOs: totalUTXOs,
499
                            totalBalance: totalBalance
500
                        });
501
                    }
502
                    //ready for next Blocktrail pub key
503
                    async.nextTick(done);
504
                });
505
            }, function(err) {
506
                done(err);
507
            });
508
        }, function(err) {
509
            //callback
510
            if (err) {
511
                //perhaps we should also reject the promise, and stop everything?
512
                if (self.settings.logging) {
513
                    console.log("error encountered when discovering funds", err);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
514
                }
515
            }
516
517
            if (self.settings.logging) {
518
                console.log("finished fund discovery: " + totalBalance + " Satoshi (in " + totalUTXOs + " outputs) " +
519
                    "found when searching " + totalAddressesGenerated + " addresses");
520
            }
521
522
            self.sweepData = {
523
                utxos: addressUTXOs,
524
                count: totalUTXOs,
525
                balance: totalBalance,
526
                addressesSearched: totalAddressesGenerated
527
            };
528
529
            //resolve the promise
530
            deferred.resolve(self.sweepData);
531
        });
532
    });
533
534
    return deferred.promise;
535
};
536
537
WalletSweeper.prototype.sweepWallet = function(destinationAddress, cb) {
538
    var self = this;
539
    var deferred = q.defer();
540
    deferred.promise.nodeify(cb);
541
542
    if (self.settings.logging) {
543
        console.log("starting wallet sweeping to address " + destinationAddress);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
544
    }
545
546
    q.when(true)
547
        .then(function() {
548
            if (!self.sweepData) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if !self.sweepData is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
549
                //do wallet fund discovery
550
                return self.discoverWalletFunds()
551
                    .progress(function(progress) {
552
                        deferred.notify(progress);
553
                    });
554
            }
555
        })
556
        .then(function() {
557
            return self.bitcoinDataClient.estimateFee();
558
        })
559
        .then(function(feePerKb) {
560
            if (self.sweepData['balance'] === 0) {
561
                //no funds found
562
                deferred.reject("No funds found after searching through " + self.sweepData['addressesSearched'] + " addresses");
563
                return deferred.promise;
564
            }
565
566
            //create and sign the transaction
567
            return self.createTransaction(destinationAddress, null, feePerKb, deferred);
568
        })
569
        .then(function(r) {
570
            deferred.resolve(r);
571
        }, function(e) {
572
            deferred.reject(e);
573
        });
574
575
    return deferred.promise;
576
};
577
578
/**
579
 * creates a raw transaction from the sweep data
580
 * @param destinationAddress        the destination address for the transaction
581
 * @param fee                       a specific transaction fee to use (optional: if null, fee will be estimated)
582
 * @param feePerKb                  fee per kb (optional: if null, use default value)
583
 * @param deferred                  a deferred promise object, used for giving progress updates (optional)
584
 */
585
WalletSweeper.prototype.createTransaction = function(destinationAddress, fee, feePerKb, deferred) {
586
    var self = this;
587
    if (this.settings.logging) {
588
        console.log("Creating transaction to address destinationAddress");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
589
    }
590
    if (deferred) {
591
        deferred.notify({
592
            message: "creating raw transaction to " + destinationAddress
593
        });
594
    }
595
596
    // create raw transaction
597
    var rawTransaction = new bitcoin.TransactionBuilder(this.network);
598
    if (this.settings.bitcoinCash) {
599
        rawTransaction.enableBitcoinCash();
600
    }
601
    var inputs = [];
602
    _.each(this.sweepData['utxos'], function(data, address) {
603
        _.each(data.utxos, function(utxo) {
604
            rawTransaction.addInput(utxo['hash'], utxo['index']);
605
            inputs.push({
606
                txid:         utxo['hash'],
607
                vout:         utxo['index'],
608
                scriptPubKey: utxo['script_hex'],
609
                value:        utxo['value'],
610
                address:      address,
611
                path:         data['path'],
612
                redeemScript: data['redeem'],
613
                witnessScript: data['witness']
614
            });
615
        });
616
    });
617
    if (!rawTransaction) {
618
        throw new Error("Failed to create raw transaction");
619
    }
620
621
    var sendAmount = self.sweepData['balance'];
622
    var outputIdx = rawTransaction.addOutput(destinationAddress, sendAmount);
623
624
    if (typeof fee === "undefined" || fee === null) {
625
        //estimate the fee and reduce it's value from the output
626
        if (deferred) {
627
            deferred.notify({
628
                message: "estimating transaction fee, based on " + blocktrail.toBTC(feePerKb) + " BTC/kb"
629
            });
630
        }
631
632
        var toHexString = function(byteArray) {
633
            return Array.prototype.map.call(byteArray, function(byte) {
634
                return ('0' + (byte & 0xFF).toString(16)).slice(-2);
635
            }).join('');
636
        };
637
638
        var calcUtxos = inputs.map(function(input) {
639
            var rs = (typeof input.redeemScript === "string" || !input.redeemScript)
640
                ? input.redeemScript : toHexString(input.redeemScript);
641
            var ws = (typeof input.witnessScript === "string" || !input.witnessScript)
642
                ? input.witnessScript : toHexString(input.witnessScript);
643
644
            return {
645
                txid: input.txid,
646
                vout: input.vout,
647
                address: input.address,
648
                scriptpubkey_hex: input.scriptPubKey,
649
                redeem_script: rs,
650
                witness_script: ws,
651
                path: input.path,
652
                value: input.value
653
            };
654
        });
655
        fee = walletSDK.estimateVsizeFee(rawTransaction.tx, calcUtxos, feePerKb);
656
    }
657
    rawTransaction.tx.outs[outputIdx].value -= fee;
658
659
    //sign and return the raw transaction
660
    if (deferred) {
661
        deferred.notify({
662
            message: "signing transaction"
663
        });
664
    }
665
    return this.signTransaction(rawTransaction, inputs);
666
};
667
668
WalletSweeper.prototype.signTransaction = function(rawTransaction, inputs) {
669
    var self = this;
670
    if (this.settings.logging) {
671
        console.log("Signing transaction");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
672
    }
673
674
    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
675
    if (this.settings.bitcoinCash) {
676
        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
677
    }
678
679
    //sign the transaction with the private key for each input
680
    _.each(inputs, function(input, index) {
681
        //create private keys for signing
682
        var primaryPrivKey =  walletSDK.deriveByPath(self.primaryPrivateKey, input['path'].replace("M", "m"), "m").keyPair;
683
        rawTransaction.sign(index, primaryPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
684
685
        if (self.backupPrivateKey) {
686
            var backupPrivKey = walletSDK.deriveByPath(self.backupPrivateKey, input['path'].replace("'", "").replace("M", "m"), "m").keyPair;
687
            rawTransaction.sign(index, backupPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
688
        }
689
    });
690
691
    if (self.backupPrivateKey) {
692
        return rawTransaction.build().toHex();
693
    } else {
694
        return rawTransaction.buildIncomplete().toHex();
695
    }
696
};
697
698
module.exports = WalletSweeper;
699